﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FMOD;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using FMODNet;
using DSP=FMOD.DSP;

namespace FMODtest
{
    public class Channel
    {
        FMOD.Channel _ch;

        float baseFrequency;
        public Channel(FMOD.Channel ch)
        {
            _ch = ch;
            SoundDevice.Queue(c => _ch.getFrequency(ref baseFrequency), false);
        }
        public float Pan
        {
            get
            {
                float _pan = 0;
                SoundDevice.Queue(c => _ch.getPan(ref _pan), false);
                return _pan;
            }
            set 
            {
                SoundDevice.Queue(c => _ch.setPan(value), true);
            }
        }
        float _pitch = 0;
        public float Pitch
        {
            get
            {
                return _pitch;
            }
            set
            {
                SoundDevice.Queue(c => SoundDevice.SafeCall(() => _ch.setFrequency(baseFrequency + value)), false);
                _pitch = value;
            }
        }
        
        public uint Position
        {
            get
            {
                if (this.IsPlaying)
                {
                    uint pos = 0;
                    SoundDevice.Queue(c => SoundDevice.SafeCall(() => _ch.getPosition(ref pos, TIMEUNIT.MS)), false);
                    return pos;
                }
                return 0;
            }
            set
            {
                
                SoundDevice.Queue(c => SoundDevice.SafeCall(() => _ch.setPosition(value, TIMEUNIT.MS)), false);
            }
        }
        public float Volume
        {
            get
            {
                float _volume = 0;
                SoundDevice.Queue(c => _ch.getVolume(ref _volume), false);
                return _volume;
            }
            set
            {
                SoundDevice.Queue(c => _ch.setVolume(value), true);
            }
        }
        
        public bool IsPlaying
        {
            get
            {
                bool isPlaying = false;
                
                SoundDevice.Queue(c => SoundDevice.SafeCall(()=>_ch.isPlaying(ref isPlaying)), false);
                return isPlaying;
            }
        }
    }

    public class SoundItem
    {   
        FileSystem fileSystem;
        FMOD.Sound sound;
        MODE openMode = MODE.CREATESTREAM | MODE.IGNORETAGS | MODE.MPEGSEARCH | MODE.ACCURATETIME | MODE.LOWMEM;
        FMOD.Channel ch;
        CREATESOUNDEXINFO _info;
        Channel _channel;
        public Channel Channel
        {
            get
            {
                return _channel;
            }
        }
        public uint Length
        {
            get
            {
                uint len = 0;

                SoundDevice.Queue(c => SoundDevice.SafeCall(()=>sound.getLength(ref len, TIMEUNIT.MS)), false);
                return len;
            }
        }
        public SoundItem(FileSystem fileManipulationSystem) 
        {
            SoundDevice.Queue(system =>
                                  {
                    
                                      fileSystem = fileManipulationSystem;
                                      _info = new CREATESOUNDEXINFO
                                                  {
                                                      userclose = fileSystem.CreateCloseCallback(),
                                                      useropen = fileSystem.CreateOpenCallback(),
                                                      userread = fileSystem.CreateReadCallback(),
                                                      userseek = fileSystem.CreateSeekCallback(),            
                                                  };
                                      _info.cbsize = Marshal.SizeOf(_info);
                                      string name = fileSystem.GetHandle().ToInt32().ToString();
                                      system.createSound(name, openMode, ref _info, ref sound);
                                      Debug.WriteLine(string.Format("Opening file {0}", fileManipulationSystem.FileDescriptor));
                                  }, true);
        }
        public SoundItem(string filename)
            : this(new FileStreamFileSystem(filename) { FileDescriptor = filename })
        {

        }
        public void Play()
        {
            
            this.ch = new FMOD.Channel();
            
            SoundDevice.Queue(system => system.playSound(CHANNELINDEX.FREE, sound, false, ref this.ch), true);


            DSP_TYPE[] dsps = new DSP_TYPE[]
                                  {
                                      //DSP_TYPE.REVERB, 
                                      //DSP_TYPE.REVERB, 
                                      //DSP_TYPE.SFXREVERB, DSP_TYPE.LOWPASS
                                      //DSP_TYPE.COMPRESSOR, 
                                      DSP_TYPE.DISTORTION
                                  };

            _channel = new Channel(this.ch);
            SoundDevice.Queue(c =>
                                  {
                                      foreach (DSP_TYPE dspType in dsps)
                                      {
                                          DSP dsp = new DSP();
                                          DSPConnection con = null;

                                          SoundDevice.SafeCall(()=>c.createDSPByType(dspType, ref dsp));
                                          this.ch.addDSP(dsp, ref con);
                                          dsp.setActive(true);
                                          int numberOfParams = 0;

                                          dsp.getNumParameters(ref numberOfParams);
                                          Debug.WriteLine(string.Format("Type: {0}", dspType));
                                          for (int i = 0; i < numberOfParams; i++)
                                          {
                                              StringBuilder name = new StringBuilder(1024);
                                              StringBuilder label = new StringBuilder(1024);
                                              StringBuilder description = new StringBuilder(1024);
                                              float min = 0, max = 0;
                                              dsp.getParameterInfo(i, name, label, description, 1024, ref min, ref max);
                                              Debug.WriteLine(string.Format("Name: {0}\tLabel: {1}\nDescription: {2}\nMin: {3}, Max: {4}", name, label, description, min, max));
                                          }
                                          dsp.setActive(true);
                                          SoundDevice.SafeCall(()=>dsp.setParameter(0, 1.0f));
                        
                                      }
                                  }, false);
            
        }

        internal void Stop()
        {
            this.ch.stop();
            this.sound.release();
        }
    }

    public class FileStreamFileSystem : FileSystem
    {
        static Dictionary<IntPtr, Stream> _streams = new Dictionary<IntPtr, Stream>();
        static Random rnd = new Random();
        IntPtr _handle = IntPtr.Zero;
        public override IntPtr GetHandle()
        {
            if (_handle != IntPtr.Zero)
                return _handle;
            IntPtr ptr = IntPtr.Zero;
            do
            {
                _handle = new IntPtr(rnd.Next(int.MaxValue));
            }
            while(_streams.ContainsKey(_handle));
            return _handle;
        }
        static Stream GetStream(IntPtr ptr)
        {
            if(_streams.ContainsKey(ptr))
                return _streams[ptr];
            return Stream.Null;
        }
        static FILE_OPENCALLBACK _openCallback = OpenCallback;
        static FILE_CLOSECALLBACK _closeCallback = CloseCallback;
        static FILE_SEEKCALLBACK _seekCallback = SeekCallback;
        static FILE_READCALLBACK _readCallback = ReadCallback;
        
        public FileStreamFileSystem(string filename) : base()
        {
            _streams.Add(this.GetHandle(), new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
        }
        static RESULT OpenCallback(string name, int unicode, ref uint filesize, ref IntPtr handle, ref IntPtr userdata)
        {
            handle = new IntPtr(int.Parse(name));
            Stream stream = GetStream(handle);//new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

            filesize = (uint)stream.Length;
            return RESULT.OK;
        }
        public override FILE_OPENCALLBACK CreateOpenCallback()
        {
            return _openCallback;
        }
        public override FILE_CLOSECALLBACK CreateCloseCallback()
        {
            return _closeCallback;
        }
        static RESULT CloseCallback(IntPtr handle, IntPtr userdata)
        {   
            using (Stream stream = GetStream(handle))
            {
                stream.Close();
                _streams.Remove(handle);
            }
            return RESULT.OK;
        }
        public override FILE_SEEKCALLBACK CreateSeekCallback()
        {
            return _seekCallback;
        }
        public static RESULT SeekCallback(IntPtr handle, int pos, IntPtr userdata)
        {
            GetStream(handle).Seek(pos, SeekOrigin.Begin);
            return RESULT.OK;
        }
        public override FILE_READCALLBACK CreateReadCallback()
        {
            return _readCallback;
        }
        static RESULT ReadCallback(IntPtr handle, IntPtr buffer, uint sizebytes, ref uint bytesread, IntPtr userdata)
        {
            Stream stream = GetStream(handle);
            byte[] data = new byte[(int)sizebytes];
            bytesread = (uint)stream.Read(data, 0, data.Length);
            Marshal.Copy(data, 0, buffer, (int)bytesread);
            return RESULT.OK;
        }
        public override void Dispose()
        {
            using (GetStream(this._handle))
            {
            }
        }
    }

    public static class SoundDevice
    {
        static void PrintUsageStatistics()
        {
            float _cpudsp = 0, _cpuStream = 0, _cpuTotal = 0, _cpuUpdate = 0;
            _system.getCPUUsage(ref _cpudsp, ref _cpuStream, ref _cpuUpdate, ref _cpuTotal);
            string cpu = string.Format("CPU Usage: dsp: {0}, streaming: {1}, update: {2}, total: {3}", _cpudsp, _cpuStream, _cpuUpdate, _cpuTotal);
            Debug.WriteLine(cpu);
            int currentallocatedRam = 0, maxAllocatedRam = 0, totalRam = 0;
            _system.getSoundRam(ref currentallocatedRam, ref maxAllocatedRam, ref totalRam);
            string memory = string.Format("Memory: current: {0}, max: {1}, total: {2}", currentallocatedRam, maxAllocatedRam, totalRam);
            Debug.WriteLine(memory);
            int numberOfChannelsPlaying = 0;
            _system.getChannelsPlaying(ref numberOfChannelsPlaying);
            string channels = string.Format("channels playing: {0}", numberOfChannelsPlaying);
            Debug.WriteLine(channels);
        }
        

        class Method
        {
            public System.Action<FMOD.System> method;
            public bool async;
            public bool executed = false;
        }
        static FMOD.System _system;
        static bool isRunning;
        static Queue<Method> queuedMethods = new Queue<Method>();
        public static void Initialize()
        {
            Thread soundSystemThread = new Thread(c =>
                                                      {
                                                          SafeCall(() => FMOD.Factory.System_Create(ref _system));
                                                          SafeCall(() => _system.init(32, INITFLAG.NORMAL, IntPtr.Zero));
#if DEBUG
                                                          Timer tmr = new Timer(delegate
                                                                                    {
                                                                                        Queue(x => SoundDevice.PrintUsageStatistics(), true);
                                                                                    }, null, 0, 30000);
#endif
                                                          startMainLoop();

                                                      });
            soundSystemThread.Start();   
        }
        public static void Queue(System.Action<FMOD.System> method, bool async)
        {
            Method m = new Method { method = method, async = async };
            queuedMethods.Enqueue(m);
            if (!async)
            {
                while (!m.executed)
                {
                    Thread.Sleep(1);
                }
            }
        }
        static void startMainLoop()
        {
            isRunning = true;
            while (isRunning)
            {
                
                Method method = null;
                while (queuedMethods.Count > 0)
                {
                    method = queuedMethods.Dequeue();
                    method.method(_system);
                    method.executed = true;
                }
                Thread.Sleep(1);
                _system.update();
            }
            _system.close();
            _system.release();
        }
        public static void Dispose()
        {
            isRunning = false;
        }
       
        public static void SafeCall(System.Func<RESULT> call)
        {
            RESULT createResult = call();
            if (createResult != RESULT.OK)
            {
                Debug.WriteLine(string.Format("Error: {0} at {1}", createResult, call.Method));
            }

        }
    }
}